/*
 *
 * Copyright (c) 2023 - future
 * @author fzh
 * @email e_341877120_mail@163.com
 * @link https://mvnrepository.com/search?q=io.github.xrfzh.cn
 *
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package cn.xrfzh.common.redis;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.xrfzh.common.utils.XrObjectConverter;
import cn.xrfzh.common.utils.XrPageUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class RedisCacheClient {

	@Resource
	private StringRedisTemplate stringRedisTemplate;

	public void set(String key, Object value, long ttl, TimeUnit timeUnit) {
		stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), ttl, timeUnit);
	}

	public void setWithLogicalExpiration(String key, Object value, long ttl, TimeUnit timeUnit) {
		// 重新封装出包含逻辑过期时间的对象
		RedisData redisData = new RedisData();
		redisData.setData(value);
		redisData.setExpiration(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(ttl)));
		// 写入Redis
		stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
	}

	public String get(String key) {
		return stringRedisTemplate.opsForValue().get(key);
	}

	public void delete(String key) {
		stringRedisTemplate.delete(key);
	}

	/**
	 * 根据id查询单个对象方法，解决缓存穿透
	 * @param keyPrefix Redis键前缀
	 * @param id 主键id
	 * @param clazz 实体类对象（如User.class）
	 * @param dbFallback 函数式编程表达式 this::getById（根据id查询单个对象方法）
	 * @param time 过期时间的数值
	 * @param timeUnit 过期时间的单位
	 * @param <K> 实体类泛型（如User）
	 * @param <ID> 主键id的数据类型（未知，引入泛型表达）
	 * @return <K> 单个实体对象
	 */
	public <K, ID> K queryByIdToDealWithCachePenetration(String keyPrefix, ID id, Class<K> clazz,
			Function<ID, K> dbFallback, Long time, TimeUnit timeUnit) {
		String key = keyPrefix + id;
		String cacheData = this.get(key);
		// 不为空且不为空白字符""
		if (StrUtil.isNotBlank(cacheData)) {
			return JSONUtil.toBean(cacheData, clazz);
		}
		// 不为空（即缓存数据为空白字符""）
		if (null != cacheData) {
			return null;
		}
		K k = dbFallback.apply(id);
		if (ObjectUtil.isEmpty(k)) {
			this.set(key, "", time, timeUnit);
			return null;
		}
		this.set(key, JSONUtil.toJsonStr(k), time, timeUnit);
		return k;
	}

	/**
	 * 根据lqw条件查询对象列表方法，lqw条件为null时查询Redis缓存，lqw条件不为null时查询数据库（保证数据的高准确性）
	 * @param keyPrefix Redis键前缀
	 * @param clazz 实体类对象（如User.class）
	 * @param lqw 查询条件（必须为LambdaQueryWrapper<K>类型且非null）
	 * @param dbFallback0 函数式编程表达式1 this::count（查询记录数方法，条件为null）
	 * @param dbFallback1 函数式编程表达式2 this::count（查询记录数方法，条件为lqw且非null）
	 * @param dbFallback2 函数式编程表达式3 this::list（根据lqw条件查询对象列表方法，条件为lqw）
	 * @param idListFallback 函数式编程表达式4 this::getIdList（根据实体对象列表获取id集合）
	 * @param <K> 实体类泛型（如User）
	 * @param <ID> id数据类型
	 * @return List<K> 实体对象列表
	 */
	public <K, ID> List<K> queryListFromRedisIfExists(String keyPrefix, Class<K> clazz, LambdaQueryWrapper<K> lqw,
			Function<LambdaQueryWrapper<K>, Long> dbFallback0, Function<LambdaQueryWrapper<K>, Long> dbFallback1,
			Function<LambdaQueryWrapper<K>, List<K>> dbFallback2, Function<List<K>, List<ID>> idListFallback) {
		List<K> kList0 = dbFallback2.apply(null);
		Map<Object, Object> cacheMap;
		K kCache;
		Map<String, Object> map = new HashMap<>();
		// 查询总记录数（lqw == null）
		long length0 = dbFallback0.apply(null);
		// 查询模糊匹配总记录数（lqw != null）
		long length1 = dbFallback1.apply(lqw);
		// 判断当前查询条件是否为null，为null查Redis缓存，不为null查数据库
		List<ID> idList = idListFallback.apply(kList0);
		if (length1 > 0 && length1 == length0) {
			for (int i = (int) length1 - 1; i >= 0; i--) {
				// 先查询Redis中是否有缓存数据，命中，直接返回
				if (ObjectUtil.isNotEmpty(stringRedisTemplate.opsForHash().values(keyPrefix + idList.get(i)))) {
					cacheMap = stringRedisTemplate.opsForHash().entries(keyPrefix + idList.get(i));
					kCache = (K) XrObjectConverter.mapToJavaBean(cacheMap, clazz);
					map.put(keyPrefix + idList.get(i), kCache);
				}
			}
			if (ObjectUtil.isNotEmpty(map) && map.size() > 0) {
				return new ArrayList<>((Collection<? extends K>) map.values());
			}
		}
		// 未命中，查询数据库返回，并存入Redis
		int j = 0;
		List<K> kList1 = dbFallback2.apply(lqw);
		for (K k : kList1) {
			Map<String, Object> kMap = BeanUtil.beanToMap(k, new HashMap<>(), CopyOptions.create()
					// 自定义字段类型转换规则，解决 例如主键id的Long类型转换为Redis的hash结构存储所需要的String类型 等诸多类型转换问题
					.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
			// 判断当前查询条件是否为null，为null时才写入Redis缓存
			if (length1 > 0 && length1 == length0) {
				stringRedisTemplate.opsForHash().putAll(keyPrefix + idList.get(j), kMap);
			}
			j++;
		}
		return kList1;
	}

	/**
	 * 一致性新增（保证数据库与Redis缓存中数据的一致性）
	 * @param keyPrefix Redis键前缀
	 * @param k 实体类对象（如user）
	 * @param dbFallback0 函数式编程表达式1 this::save
	 * @param dbFallback1 函数式编程表达式2 this::list
	 * @param idListFallback 函数式编程表达式3 this::getIdList
	 * @param <K> 实体类泛型（如User）
	 * @param <ID> id数据类型
	 * @return boolean 新增成功或失败
	 */
	@Transactional
	public <K, ID> boolean insertWithConsistency(String keyPrefix, K k,
			Function<LambdaQueryWrapper<K>, List<K>> dbFallback0, Function<K, Boolean> dbFallback1,
			Function<List<K>, List<ID>> idListFallback) {
		List<K> kList = dbFallback0.apply(null);
		boolean flag = dbFallback1.apply(k);
		List<ID> idList = idListFallback.apply(kList);
		for (ID _id : idList) {
			try {
				stringRedisTemplate.delete(keyPrefix + _id.toString());
			}
			catch (Exception e) {
				log.error("Redis缓存删除失败");
				e.printStackTrace();
			}
		}
		return flag;
	}

	/**
	 * 一致性更新（保证数据库与Redis缓存中数据的一致性）
	 * @param keyPrefix Redis键前缀
	 * @param k 实体类对象（如user）
	 * @param dbFallback0 函数式编程表达式1 this::updateById
	 * @param dbFallback1 函数式编程表达式2 this::list
	 * @param idListFallback 函数式编程表达式3 this::getIdList
	 * @param <K> 实体类泛型（如User）
	 * @param <ID> id数据类型
	 * @return boolean 更新成功或失败
	 */
	@Transactional
	public <K, ID> boolean updateWithConsistency(String keyPrefix, K k,
			Function<LambdaQueryWrapper<K>, List<K>> dbFallback0, Function<K, Boolean> dbFallback1,
			Function<List<K>, List<ID>> idListFallback) {
		List<K> kList = dbFallback0.apply(null);
		boolean flag = dbFallback1.apply(k);
		List<ID> idList = idListFallback.apply(kList);
		for (ID _id : idList) {
			try {
				stringRedisTemplate.delete(keyPrefix + _id.toString());
			}
			catch (Exception e) {
				log.error("Redis缓存删除失败");
				e.printStackTrace();
			}
		}
		return flag;
	}

	/**
	 * 一致性删除（保证数据库与Redis缓存中数据的一致性）
	 * @param keyPrefix Redis键前缀
	 * @param id 主键id
	 * @param dbFallback0 函数式编程表达式1 this::removeById
	 * @param dbFallback1 函数式编程表达式2 this::list
	 * @param idListFallback 函数式编程表达式3 this::getIdList
	 * @param <K> 实体类泛型（如User）
	 * @param <ID> id数据类型
	 * @return boolean 删除成功或失败
	 */
	@Transactional
	public <K, ID> boolean deleteWithConsistency(String keyPrefix, ID id,
			Function<LambdaQueryWrapper<K>, List<K>> dbFallback0, Function<ID, Boolean> dbFallback1,
			Function<List<K>, List<ID>> idListFallback) {
		List<K> kList = dbFallback0.apply(null);
		boolean flag = dbFallback1.apply(id);
		List<ID> idList = idListFallback.apply(kList);
		for (ID _id : idList) {
			try {
				stringRedisTemplate.delete(keyPrefix + _id.toString());
			}
			catch (Exception e) {
				log.error("Redis缓存删除失败");
				e.printStackTrace();
			}
		}
		return flag;
	}

	/**
	 * 基于Redis缓存改进的查询列表方法
	 * @param key Redis键
	 * @param ttl 缓存过期时间
	 * @param timeUnit 过期时间单位
	 * @param clazz 对象类（如：User.class）
	 * @param lqw MyBatisPlus条件构造器 LambdaQueryWrapper
	 * @param dbFallback 函数式编程表达式（如：this::list）
	 * @param <K> 实体类泛型（如：User）
	 * @return List<K> 实体对象集合（如：userList）
	 */
	public <K> List<K> queryAllFromRedisIfExists(String key, Long ttl, TimeUnit timeUnit, Class<K> clazz,
			LambdaQueryWrapper<K> lqw, Function<LambdaQueryWrapper<K>, List<K>> dbFallback) {
		List<K> kList;
		// 根据条件查询时，查数据库直接返回（数据准确性要求较高）
		if (lqw != null) {
			kList = dbFallback.apply(lqw);
			return kList;
		}
		// 优先查询Redis缓存中的数据
		String kListCache = stringRedisTemplate.opsForValue().get(key);
		// 判断是否为空
		if (StrUtil.isNotBlank(kListCache)) {
			// 不为空，将json字符串解析为对象集合并返回
			kList = JSON.parseArray(kListCache, clazz);
			return kList;
		}
		// 为空，查询数据库并存入Redis缓存然后返回
		kList = dbFallback.apply(null);
		String jsonString = JSON.toJSONString(kList);
		stringRedisTemplate.opsForValue().set(key, jsonString, ttl, timeUnit);
		return kList;
	}

	/**
	 * 基于Redis缓存改进的分页查询方法
	 * @param keyPrefix Redis键前缀
	 * @param ttl 缓存过期时间
	 * @param timeUnit 过期时间单位
	 * @param currentPage 当前页数
	 * @param pageSize 每页记录数
	 * @param lqw MyBatisPlus条件构造器 LambdaQueryWrapper
	 * @param dbFallback 函数式编程表达式（如：this::list）
	 * @param <K> 实体类泛型（如：User）
	 * @return XrPageUtil 分页对象
	 */
	public <K> XrPageUtil queryByPageFromRedisIfExists(String keyPrefix, Long ttl, TimeUnit timeUnit, int currentPage,
			int pageSize, LambdaQueryWrapper<K> lqw, Function<LambdaQueryWrapper<K>, List<K>> dbFallback) {
		List<K> kList;
		// 根据条件查询时，查数据库直接返回（数据准确性要求较高）
		if (lqw != null) {
			Page<?> page = PageHelper.startPage(currentPage, pageSize);
			kList = dbFallback.apply(lqw);
			int total = (int) page.getTotal();
			return XrPageUtil.respByPage(kList, currentPage, pageSize, total);
		}
		// 优先查询Redis缓存中的数据
		String key = keyPrefix + ":" + "current:" + currentPage + ":" + "size:" + pageSize;
		String pageJsonString = stringRedisTemplate.opsForValue().get(key);
		// 判断是否为空
		if (StrUtil.isNotBlank(pageJsonString)) {
			JSONObject jsonObject = JSONObject.parseObject(pageJsonString);
			if (jsonObject != null) {
				JSONArray recordsArray = jsonObject.getJSONArray("records");
				DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
				for (int i = 0; i < recordsArray.size(); i++) {
					JSONObject record = recordsArray.getJSONObject(i);
					Long timestamp1 = record.getLong("createTime");
					if (timestamp1 != null) {
						LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp1),
								ZoneId.systemDefault());
						String timeStr1 = dateTime.format(formatter);
						record.put("createTime", timeStr1);
					}
					Long timestamp2 = record.getLong("updateTime");
					if (timestamp2 != null) {
						LocalDateTime updateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp2),
								ZoneId.systemDefault());
						String timeStr2 = updateTime.format(formatter);
						record.put("updateTime", timeStr2);
					}
				}
				String newPageJsonString = jsonObject.toString();
				return JSONUtil.toBean(newPageJsonString, XrPageUtil.class);
			}
		}
		// 为空，查询数据库并存入Redis缓存然后返回
		Page<?> page = PageHelper.startPage(currentPage, pageSize);
		kList = dbFallback.apply(null);
		int total = (int) page.getTotal();
		XrPageUtil xrPageUtil = XrPageUtil.respByPage(kList, currentPage, pageSize, total);
		String jsonString = JSON.toJSONString(xrPageUtil);
		stringRedisTemplate.opsForValue().set(key, jsonString, ttl, timeUnit);
		return xrPageUtil;
	}

	/**
	 * 清除Redis缓存
	 * @param keyPrefix 缓存的键前缀
	 */
	public void clearRedisCache(String keyPrefix) {
		// 定义Lua脚本
		String luaScript = "local keys = redis.call('keys', '" + keyPrefix + "*')\n" + "for i, key in ipairs(keys) do\n"
				+ "    redis.call('del', key)\n" + "end";
		// 使用StringRedisTemplate执行Lua脚本
		stringRedisTemplate.execute((RedisCallback<Object>) connection -> {
			connection.eval(luaScript.getBytes(), ReturnType.INTEGER, 0);
			return null;
		});
		// 关闭StringRedisTemplate
		Objects.requireNonNull(stringRedisTemplate.getConnectionFactory()).getConnection().close();
	}

}
